/*****************************************************************************
 * $LastChangedDate: 2011-04-23 21:07:07 -0400 (Sat, 23 Apr 2011) $
 * @file
 * @author  Jim E. Brooks  http://www.palomino3d.org
 * @brief   Medium particles and particle-system.
 * @remarks This implementation was kept simple.  For now, decided to omit
 *          implementing a particle updating mechanism and per-particle attributes.
 *//*
 * LEGAL:   COPYRIGHT (C) 2008 JIM E. BROOKS
 *          THIS SOURCE CODE IS RELEASED UNDER THE TERMS
 *          OF THE GNU GENERAL PUBLIC LICENSE VERSION 2 (GPL 2).
 *****************************************************************************/

//##############################################################################
// TODO/FIXME/DEPRECATED:
// This particle-system should be eliminated.
// It doesn't produce good results for missile trails.
// Problems are that point-sprite width is limited by hw
// and point-sprite widths aren't scaled (projected) accurately.
//##############################################################################

#define FX_PARTSYS_MEDIUM_CC 1
#include <vector>
#include "base/module.hh"
using namespace base;
#include "math/module.hh"
#include "math/funcs_vector.hh"
using namespace math;
#include "object/module.hh"
using namespace object;
#include "graph/module.hh"
#include "graph/scene_graph.hh"
using namespace graph;
#include "world/module.hh"
using namespace world;
#include "fx/module.hh"
#include "fx/partsys_medium.hh"
#include <osg/Point>
#include <osg/PointSprite>
#include <osg/Version>

namespace fx {

INTERN const uint MEDIUM_PARTICLES_LIMIT   = 5000;
INTERN const uint MEDIUM_PARTICLES_RESERVE = 5000;

////////////////////////////////////////////////////////////////////////////////
/////////////////////////  MediumParticleDrawable  //////////////////////////////
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
/// @brief This is an OSG Drawable object that directly renders point-sprites.
///
/// The reason for directly-rendering is to that the size of every point-sprite
/// needs to be computed individually.  Issuing glPointSize() for every particle
/// is faster and less-cumbersome than composing nodes with various point sizes.
///
/// When OSG has called drawImplementation(), OSG has set OpenGL state
/// for rendering OpenGL "point sprites".
///
class MediumParticleDrawable : public osg::Drawable
{
private:
    /*****************************************************************************
     * ctor subroutine.
     *****************************************************************************/
    void Init( void )
    {
        SET_TYPESIG(this,TYPESIG_MEDIUM_PARTICLE_DRAWABLE);

        // Pre-allocate.
        mVertexArray.reserve( MEDIUM_PARTICLES_RESERVE );

        // Disable display list else only the OpenGL calls
        // of the first particle would be captured.
        setUseDisplayList( false );
    }

public:
    META_Object(Palomino,MediumParticleDrawable)  // OSG macro

    /*****************************************************************************
     * ctor/dtor.
     * @param   color
     *          Applied to all particles.
     * @param   range
     *          Distance in world space where particle is scaled-down to mediumest size.
     * @param   minWidth, maxWidth
     *          Limits of point size scaled by distance of their vertex from viewpoint.
     *          This simulates 3D:2D projection.
     *****************************************************************************/
    MediumParticleDrawable( const RGBA color, const fp range,  const fp minWidth, const fp maxWidth )
    :   osg::Drawable(),
        mColor(color),
        mRange(range),
        mRangeInv(1.0f / range),
        mMinWidth(minWidth),
        mMaxWidth(maxWidth),
        mVertexArray(),
        mBoundingBox()
    {
        Init();
    }

    // (for OSG)
    MediumParticleDrawable( void )
    :   osg::Drawable(),
        mColor(),
        mRange(1000.0f),
        mRangeInv(1000.0f),
        mMinWidth(1.0f),
        mMaxWidth(64.0f),
        mVertexArray(),
        mBoundingBox()
    {
    }

    // (unexpected)
    MediumParticleDrawable( const MediumParticleDrawable& other, const osg::CopyOp& copyop = osg::CopyOp::SHALLOW_COPY)
    :   osg::Drawable(),
        mColor(other.mColor),
        mRange(other.mRange),
        mRangeInv(other.mRangeInv),
        mMinWidth(other.mMinWidth),
        mMaxWidth(other.mMaxWidth),
        mVertexArray(other.mVertexArray),  // TOO SLOW
        mBoundingBox(other.mBoundingBox)
    {
    ASSERT(false);  // copying vertex array would be too SLOW, catch if copy ctor is ever called
        Init();
    }

    virtual ~MediumParticleDrawable()
    {
        INVALIDATE_TYPESIG(this,TYPESIG_MEDIUM_PARTICLE_DRAWABLE);
    }

    /*****************************************************************************
     * Draw array of medium particles.
     *****************************************************************************/
    virtual void drawImplementation( osg::RenderInfo& renderInfo ) const
    {
    CHECK_TYPESIG(this,TYPESIG_MEDIUM_PARTICLE_DRAWABLE);

        const uint vcount = mVertexArray.size();
        if ( UX( vcount == 0 ) )
            return;

        // The inverse of view matrix has the correct eye position.
        const osg::Vec3  eye  = renderInfo.getCurrentCamera()->getInverseViewMatrix().getTrans();
        const osg::Vec3* v    = &mVertexArray[0];     // first vertex
      //const osg::Vec3* vEnd = &mVertexArray[vcount];  // wrong: operator[] will throw exception!
        const osg::Vec3* vEnd = v + vcount;             // end vertex (end as in STL)

        // Save current color.
        GLfloat savedColor[4];
        glGetFloatv( GL_CURRENT_COLOR, savedColor );

        // Set color.
        GLfloat color[4];
        mColor.Get4f<GLfloat>( color );  // RGBA object --> color[4]
        glColor4fv( color );

        // For each vertex.
        for ( ; v < vEnd; ++v )
        {
        ASSERT( not mVertexArray.empty() );

            const fp dist      = Min<fp>( mRange, Distance(eye,*v) );
            const fp ratio     = (mRange - dist) * mRangeInv;
            const fp pointSize = Max<fp>( mMinWidth, std::pow(ratio,4.0f) * mMaxWidth );
            glPointSize( pointSize );
            glBegin( GL_POINTS );
                glVertex3fv( reinterpret_cast<const GLfloat*>( v ) );
            glEnd();
        }

        // Restore color.
        glColor4fv( savedColor );
    }

    /*****************************************************************************
     * Compute bound of medium particles.
     * Which is the fastest?
     * Bounding all point-sprites in one box or dividing them into multiple boxes (?).
     *****************************************************************************/
#if OSG_VERSION_LESS_THAN(3,0,0)
    osg::BoundingBox computeBound() const
    {
    CHECK_TYPESIG(this,TYPESIG_MEDIUM_PARTICLE_DRAWABLE);

        return mBoundingBox;
    }
#endif

    /*****************************************************************************
     * Add particle.
     *****************************************************************************/
    void
    AttachParticle( const osg::Vec3& v )
    {
    CHECK_TYPESIG(this,TYPESIG_MEDIUM_PARTICLE_DRAWABLE);

        // Too many particles will slow FPS.
        if ( EX( mVertexArray.size() < MEDIUM_PARTICLES_LIMIT ) )
        {
            // Append vertex.
            mVertexArray.push_back( v );

            // Expand bounding box.
            mBoundingBox.expandBy( v );

            // Force recomputing bounding box.
            dirtyBound();
        }
    }

    /*****************************************************************************
     * @return Vertex count (amount of particles).
     *****************************************************************************/
    uint
    GetVertexCount( void ) const
    {
    CHECK_TYPESIG(this,TYPESIG_MEDIUM_PARTICLE_DRAWABLE);

        return mVertexArray.size();
    }

    /*****************************************************************************
     * Set color of all particles.
     *****************************************************************************/
    void
    SetColorAll( const RGBA color )
    {
    CHECK_TYPESIG(this,TYPESIG_MEDIUM_PARTICLE_DRAWABLE);

        mColor = color;
    }

private:
    RGBA                   mColor;         ///< color of all point sprites
    const fp               mRange;         ///< width of point-sprite is minimized at limit of range
    const fp               mRangeInv;
    const fp               mMinWidth;      ///< minimum size of point-sprites
    const fp               mMaxWidth;      ///< maximum size of point-sprites
    std::vector<osg::Vec3> mVertexArray;   ///< not osg::Vec3Array which lacks swap()
    osg::BoundingBox       mBoundingBox;   ///< no need for ref_ptr (isn't derived from osg::Referenced)
    DECLARE_TYPESIG(TYPESIG_MEDIUM_PARTICLE_DRAWABLE);
};

////////////////////////////////////////////////////////////////////////////////
///////////////////////  MediumParticleSystem::Args  ////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * Args for MediumParticleSystem ctor.
 * @param   nodeSort
 *          For state-sorting scene-graph.
 *          Texture can be specified in stateset of NodeSort.
 * @param   color
 *          Of all particles.
 * @param   geoRange
 *          Range in 3D space when 2D point sprites are scaled to min,max sizes.
 *          This simulates a 3D appearance.
 * @param   minWidth, maxWidth
 *          min,max of width of point sprites (limited by hardware, 64 pixels max is typical).
 * Example:
 * new MediumParticleSystem( MediumParticleSystem::Args( Texture::NewTextureAsStateSet( "smoke.png" ) ) );
 *****************************************************************************/
MediumParticleSystem::Args::Args( const Milliseconds lifetime,
                                  const bool timerEnabled,
                                  const NodeSort& nodeSort,
                                  const RGBA color,
                                  const Meter geoRange,
                                  const fp minWidth,
                                  const fp maxWidth )
:   ParticleSystem::Args(lifetime,timerEnabled),
    mNodeSort(nodeSort),
    mColor(color),
    mRange(world::conv::Meters2Sim(geoRange)),
    mMinWidth(minWidth),
    mMaxWidth(maxWidth)
{
CHECK_TYPESIG(&nodeSort,TYPESIG_NODE_SORT);
    // NOP
}

////////////////////////////////////////////////////////////////////////////////
///////////////////////////  MediumParticleSystem  //////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * ctor/dtor.
 *****************************************************************************/
MediumParticleSystem::MediumParticleSystem( const Args& args )
:   ParticleSystem(args),
    // OSG components:
    mAttachedToSceneGraph(false),
    mNodeSort(args.mNodeSort),
    mStateSet(args.mNodeSort.GetStateSet()),
    mGeode(new osg::Geode),
    mGeom(new osg::Geometry),
    mMediumParticleDrawable(new MediumParticleDrawable(args.mColor,args.mRange,args.mMinWidth,args.mMaxWidth))
{
CHECK_TYPESIG(&args.mNodeSort,TYPESIG_NODE_SORT);

    SET_TYPESIG(this,TYPESIG_MEDIUM_PARTICLE_SYSTEM);

    // Compose OSG components:
    // MediumParticleDrawable is the reason why some node components are omitted.
    mGeode->setStateSet( mStateSet.get() );
    mGeode->addDrawable( mMediumParticleDrawable.get() );

    // Configure point sprite:
    mStateSet->setAttribute( new osg::Point );
    mStateSet->setTextureAttributeAndModes( 0, new osg::PointSprite, osg::StateAttribute::ON );

    // (The caller should have passed a stateset with a texture attached to it)
}

MediumParticleSystem::~MediumParticleSystem()
{
    // Detach from scene-graph.
    if ( mAttachedToSceneGraph )
    {
        GET_SCENE_GRAPH().DetachNode( mGeode.get() );
    }

    INVALIDATE_TYPESIG(this,TYPESIG_MEDIUM_PARTICLE_SYSTEM);
}

/*****************************************************************************
 * Append MediumParticle.
 * Time-of-death is absolute (add current time to lifetime).
 *****************************************************************************/
void
MediumParticleSystem::AttachParticle( const MediumParticle& mediumParticle )
{
CHECK_TYPESIG(this,TYPESIG_MEDIUM_PARTICLE_SYSTEM);
CHECK_TYPESIG(&mNodeSort,TYPESIG_NODE_SORT);

    // Lazy-attach to scene-graph.
    if ( UX( not mAttachedToSceneGraph ) )
    {
        mAttachedToSceneGraph = true;
        GET_SCENE_GRAPH().AttachNode( mGeode.get(), mNodeSort );
    }

    // Add vertex to Drawable.
    mMediumParticleDrawable->AttachParticle( mediumParticle.mPosition );
}

/*****************************************************************************
 * @return Amount of particles.
 *****************************************************************************/
uint
MediumParticleSystem::GetParticleCount( void )
{
CHECK_TYPESIG(this,TYPESIG_MEDIUM_PARTICLE_SYSTEM);

    return mMediumParticleDrawable->GetVertexCount();
}

/*****************************************************************************
 * Set color of all particles.
 *****************************************************************************/
void
MediumParticleSystem::SetColorAll( const RGBA color )
{
CHECK_TYPESIG(this,TYPESIG_MEDIUM_PARTICLE_SYSTEM);

    return mMediumParticleDrawable->SetColorAll( color );
}

} // namespace fx
